import random
import backend


class Brain(object):
    """
    the base class for all decision making AI objects, but you probably
    don't want to actually directly use this. just subclass it
    """

    def __init__(self):
        pass

    def __eq__(self, other):
        return (isinstance(other, self.__class__) and
                self.__dict__ == other.__dict__)

    def __ne__(self, other):
        return not self.__eq__(other)


class DefaultAI(Brain):
    """
    default AI object for units
    """

    def __init__(self):
        super(DefaultAI, self).__init__()

    def turn(self, percepts):
        """
        decide which action to take this turn

        args:
            percepts: a dict containing information that the unit perceives

        return:
            a direction to move in
        """
        # TODO: beef up this return value to be able to return different actions
        #return simple_omniscient_reflex(percepts)
        return targeted_reflex(percepts)

    def __repr__(self):
        return 'DefaultAI()'

def simple_omniscient_reflex(percepts):
    """
    a simple reflex agent with omniscience. always moves toward the player

    args:
        percepts: a dict containing information that the unit perceives, must contain at least:
            'self': location of this unit
            'player': location of the player
            'scene': the scene object this unit is in

    returns:
        a direction for movement
    """
    #proximity = percepts['self'].location.distance(percepts['player']) < 3
    return a_star(percepts['self'].location, percepts['player'].location, percepts['scene'])[0] - percepts['self'].location

def targeted_reflex(percepts, sight_radius=6):
    """
    only chases you if it detects you
    """
    if percepts['self'].target:
        return simple_omniscient_reflex(percepts)
    else:
        if percepts['self'].location.distance(percepts['player'].location) <= sight_radius:
            detected = backend.roll(base=percepts['self'].skills['detection'], t=percepts['player'].skills['stealth'])
            if detected >= 0:
                print '{} spotted you. (by {})'.format(percepts['self'].name, detected)
                percepts['self'].target = percepts['player']
                return simple_omniscient_reflex(percepts)
        return 0, 0


def a_star(start, goal, graph, unit_check=True):
    """
    make a path from the start to the goal

    args:
        start: the starting Location
        goal: the Location to path to
        graph: a scene to navigate through

    returns:
        a list of Locations representing the found path, or the starting location wrapped in a list if no path is
            found
    """
    open_set = {start}
    closed_set = set()
    came_from = {}

    g_score = {start: 0}
    f_score = {start: start.distance(goal)}

    i = 0

    def retrace_path(c):
        def parent_gen(c):
            while c:
                yield c
                if came_from[c] != start:
                    c = came_from[c]
                else:
                    break
        result = [element for element in parent_gen(c)]
        result.reverse()
        return result

    while open_set:
        i += 1
        current = min(open_set, key=f_score.get)
        if current == goal:
            #print i
            return retrace_path(current)
        open_set.remove(current)
        closed_set.add(current)
        for neighbor in graph.pathable_neighborhood(current, unit_check):
            tentative_g_score = g_score[current] + 1
            if neighbor in closed_set:
                if tentative_g_score >= g_score[neighbor]:
                    continue
            if neighbor not in open_set or tentative_g_score < g_score[neighbor]:
                came_from[neighbor] = current
                g_score[neighbor] = tentative_g_score
                f_score[neighbor] = g_score[neighbor] + neighbor.distance(goal)
                if neighbor not in open_set:
                    open_set.add(neighbor)
    return [start]

def set_up():
    import backend
    graph = backend.Scene(50, 50, floor_type=0, wall_type=1, name='Test')
    graph[backend.Location((25, 26))] = 1
    graph[backend.Location((26, 26))] = 1
    graph[backend.Location((24, 26))] = 1
    u = backend.Unit(location=backend.Location((24, 25)))
    graph.units.add(u)
    #graph.grid[27] = [1] * 50
    return backend.Location((25, 25)), backend.Location((26, 40)), graph

if __name__ == '__main__':
    import time
    import random
    random.seed(4)
    graph = backend.Scene(50, 50, floor_type=0, wall_type=1, name='Test')
    start = backend.Location((10, 10))
    end = backend.Location((35, 15))
    #import timeit
    #print timeit.timeit('a_star(*set_up())', 'from __main__ import a_star, set_up', number=100)
    t = time.time()
    for _ in xrange(1000):
        a_star(start, end, graph)
    print '{} seconds elapsed'.format(time.time() - t)